package jeeobserver.server;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import jeeobserver.server.RequestParameter;
import jeeobserver.server.DatabaseFile.DatabaseEntity;
import jeeobserver.server.DatabaseFile.DatabaseFileFixedRecord;
import jeeobserver.server.DatabaseFile.HeaderEntity;
import jeeobserver.utilities.DatabaseUtilities;

final class DatabaseFileStatistics<T extends DatabaseFileStatistics.DatabaseStatisticsEntity> extends DatabaseFile.DatabaseFileFixedRecord {

	 private static final int PAGE_SIZE = 100;

	 private static final int OPTIMIZATION_THRESHOLD = 10000;

	 private static final int BULK_SIZE = 10000;

	 private Map<Date, Set<Long>> indexTime;

	 private Map<Integer, Set<Long>> indexElement;

	 private final String path;

	 private final int samplingPeriod;

	 DatabaseFileStatistics(String path, Class type, int recordLenght, int samplingPeriod) throws IOException {
			super(path + ".dat", type, recordLenght);

			StatisticsHeaderEntity headerEntity = this.getHeader();

			if (headerEntity.getSamplingPeriod() != samplingPeriod) {
				 headerEntity.setSamplingPeriod(samplingPeriod);
				 this.updateHeader(headerEntity);
			}

			this.path = path;

			this.samplingPeriod = samplingPeriod;

			//Load into memory time and element index
			this.calculateIndexes();
	 }

	 public synchronized void persist(Collection<DatabaseStatisticsEntity> entities) throws IOException {

			long position = this.length();

			this.seek(position);

			this.write(entities);

			for (DatabaseStatisticsEntity entity : entities) {
				 this.updateIndexes(entity, position);
				 position = position + entity.getSize();
			}

			if (this.getHeader().getUnoptimizedRows() >= OPTIMIZATION_THRESHOLD) {
				 System.out.println("Optimize: " + this.getHeader().getUnoptimizedRows());
				 this.optimize();
			}
	 }

	 @Override
	 public synchronized void write(Collection entities) throws IOException {

			super.write(entities);

			//Update unoptimized rows to optimize
			StatisticsHeaderEntity header = this.getHeader();

			header.setUnoptimizedRows(header.getUnoptimizedRows() + entities.size());

			this.updateHeader(header);

	 }

	 private void calculateIndexes() throws IOException {

			this.indexTime = new HashMap();
			this.indexElement = new HashMap();

			Collection<DatabaseStatisticsEntity> statistics;

			this.reset();

			while (!(statistics = (Collection<DatabaseStatisticsEntity>) this.read(PAGE_SIZE)).isEmpty()) {
				 for (DatabaseStatisticsEntity statisticEntity : statistics) {
						updateIndexes(statisticEntity, this.getPointer() - statisticEntity.getSize());
				 }
			}

	 }

	 private synchronized void updateIndexes(DatabaseStatisticsEntity statisticEntity, long position) throws IOException {
			long page = (position - HeaderEntity.BYTES_SIZE) / (statisticEntity.getSize() * PAGE_SIZE);

			//Time index (sampled by hour)
			Date indexDate = DatabaseUtilities.calculateStatisticTimestamp(statisticEntity.getDate(), RequestParameter.SAMPLING_PERIOD_HOUR);

			if (!this.indexTime.containsKey(indexDate)) {
				 this.indexTime.put(indexDate, new TreeSet<Long>());
			}
			this.indexTime.get(indexDate).add(page);

			//Element index
			if (!this.indexElement.containsKey(statisticEntity.getElementId())) {
				 this.indexElement.put(statisticEntity.getElementId(), new TreeSet<Long>());
			}
			this.indexElement.get(statisticEntity.getElementId()).add(page);
	 }

	 public synchronized int delete(Set<Integer> elementIds, Date dateFrom, Date dateTo) throws IOException {
			int result = 0;

			this.reset();

			//Load indexPages
			Set<Long> indexPages = this.getIndexPages(elementIds, dateFrom, dateTo);

			//Remove pages from indexes
			for (Date id : indexTime.keySet()) {
				 indexTime.get(id).removeAll(indexPages);
			}

			for (int id : indexElement.keySet()) {
				 indexElement.get(id).removeAll(indexPages);
			}

			int deletions = 0;

			for (long page : indexPages) {

				 this.seek(HeaderEntity.BYTES_SIZE + (page * (PAGE_SIZE * this.getRecordLenght())));

				 int count = 0;
				 DatabaseStatisticsEntity entity;
				 while (((entity = (DatabaseStatisticsEntity) this.read()) != null) && count < PAGE_SIZE) {

						boolean filterPassed = false;
						if (entity.getStatus() == DatabaseStatisticsEntity.STATUS_ACTIVE
										&& elementIds.contains(entity.getElementId())
										&& (dateFrom != null && dateFrom.before(entity.getDate()))
										&& (dateTo != null && dateTo.after(entity.getDate()))) {
							 filterPassed = true;
						}

						if (filterPassed) {
							 entity.setStatus(DatabaseStatisticsEntity.STATUS_DELETED);
							 this.writePervious(entity);
							 deletions = deletions + 1;
						} else {
							 //Refill indexes
							 updateIndexes(entity, this.getPointer() - entity.getSize());
						}

						count = count + 1;
				 }
			}

			StatisticsHeaderEntity header = this.getHeader();
			header.setUnoptimizedRows(header.getUnoptimizedRows() + deletions);
			this.updateHeader(header);

			return result;

	 }

	 public synchronized <T> List<T> search(Set<Integer> elementIds, Date dateFrom, Date dateTo) throws IOException {

			this.reset();

			List<T> results = new ArrayList<T>();

			//Load indexPages
			Set<Long> indexPages = this.getIndexPages(elementIds, dateFrom, dateTo);

			for (long page : indexPages) {

				 this.seek(HeaderEntity.BYTES_SIZE + (page * (PAGE_SIZE * this.getRecordLenght())));

				 for (DatabaseStatisticsEntity entity : (Collection<DatabaseStatisticsEntity>) this.read(PAGE_SIZE)) {
						boolean filterPassed = false;
						if (entity.getStatus() == DatabaseStatisticsEntity.STATUS_ACTIVE
										&& elementIds.contains(entity.getElementId())
										&& (dateFrom != null && dateFrom.before(entity.getDate()))
										&& (dateTo != null && dateTo.after(entity.getDate()))) {
							 filterPassed = true;
						}

						if (filterPassed) {
							 results.add((T) entity);
						}
				 }
			}

			return results;
	 }

	 private synchronized Set<Long> getIndexPages(Set<Integer> elementIds, Date dateFrom, Date dateTo) {

			//Load indexPages
			Set<Long> indexTimePages = new HashSet();
			Set<Long> indexElementPages = new HashSet();

			//Load time index
			for (Date id : indexTime.keySet()) {
				 if ((dateFrom == null || id.getTime() >= dateFrom.getTime()) && (dateTo == null || id.getTime() < dateTo.getTime())) {
						indexTimePages.addAll(indexTime.get(id));
				 }
			}

			//Load element index
			for (int id : indexElement.keySet()) {
				 if (elementIds.contains(id)) {
						indexElementPages.addAll(indexElement.get(id));
				 }
			}

			//Intersect time and element indexes
			indexTimePages.retainAll(indexElementPages);

			return indexTimePages;
	 }

	 private synchronized void optimize() throws IOException {

			//1 Check for duplicates and remove deleted rows

			DatabaseStatisticsEntity statisticEntity;

			Map<Integer, OptimizedRow> duplicatesMap = new HashMap();

			this.reset();

			while ((statisticEntity = (DatabaseStatisticsEntity) this.read()) != null) {
				 if (statisticEntity.getStatus() != DatabaseStatisticsEntity.STATUS_DELETED) {

						int id = (statisticEntity.getElementId() + "|" + DatabaseUtilities.calculateStatisticTimestamp(statisticEntity.getDate(), this.samplingPeriod).getTime()).hashCode();

						if (!duplicatesMap.containsKey(id)) {
							 duplicatesMap.put(id, new OptimizedRow(statisticEntity.getDate(), statisticEntity.getElementId(), this.getPointer() - statisticEntity.getSize()));
						} else {
							 duplicatesMap.get(id).addPosition(this.getPointer() - statisticEntity.getSize());
						}
				 }
			}

			//2 Create working file

			DatabaseFileFixedRecord fileWorking = new DatabaseFileFixedRecord(this.path + ".working", this.getType(), this.getRecordLenght());

			//If working file already exists append

			fileWorking.seek(fileWorking.length());
			List<DatabaseStatisticsEntity> buffer = new ArrayList();

			for (OptimizedRow row : new TreeSet<OptimizedRow>(duplicatesMap.values())) {

				 statisticEntity = null;

				 for (long position : row.getPositions()) {

						this.seek(position);
						if (statisticEntity == null) {
							 statisticEntity = (DatabaseStatisticsEntity) this.read();
						} else {
							 statisticEntity.add((DatabaseStatisticsEntity) this.read());
						}
				 }

				 buffer.add(statisticEntity);

				 if (buffer.size() >= BULK_SIZE) {
						fileWorking.write(buffer);
						buffer.clear();
				 }
			}

			fileWorking.write(buffer);
			buffer.clear();

			//3 Update statistics file

			//If working file already exists append
			this.clear();
			this.setLength(fileWorking.length());
			this.reset();
			fileWorking.reset();

			while ((statisticEntity = (DatabaseStatisticsEntity) fileWorking.read()) != null) {
				 buffer.add(statisticEntity);

				 if (buffer.size() >= BULK_SIZE) {
						this.write(buffer);
						buffer.clear();
				 }
			}

			this.write(buffer);
			buffer.clear();

			fileWorking.delete();


			//4 Update unoptimized rows to optimize
			StatisticsHeaderEntity header = this.getHeader();

			header.setUnoptimizedRows(0);

			this.updateHeader(header);


			//5 recalculate indexes

			this.calculateIndexes();
	 }

	 @Override
	 public synchronized void clear() throws IOException {
			super.clear();
			this.indexElement.clear();
			this.indexTime.clear();
	 }

	 synchronized Set<Integer> getActiveElementIds() {

			Set<Integer> result = new HashSet<Integer>();

			for (Integer id : indexElement.keySet()) {
				 if (!indexElement.get(id).isEmpty()) {
						result.add(id);
				 }
			}

			return result;
	 }

	 public synchronized StatisticsHeaderEntity getHeader() throws IOException {

			long position = this.getPointer();

			this.seek(0);

			byte[] header = new byte[HeaderEntity.BYTES_SIZE];
			this.read(header);

			this.seek(position);

			return new StatisticsHeaderEntity(ByteBuffer.wrap(header));
	 }

	 public synchronized void updateHeader(StatisticsHeaderEntity headerEntity) throws IOException {
			long position = this.getPointer();
			this.seek(0);
			this.write(headerEntity.getBytes());
			this.seek(position);
	 }

	 public interface DatabaseStatisticsEntity extends DatabaseEntity {

			public static final byte STATUS_ACTIVE = 0;

			public static final byte STATUS_DELETED = 1;

			public abstract Date getDate();

			public abstract int getElementId();

			public abstract byte getStatus();

			public abstract void setStatus(byte status);

			public abstract void add(DatabaseStatisticsEntity entity);
	 }

	 private static class StatisticsHeaderEntity extends HeaderEntity implements DatabaseEntity {

			private static final int BYTES_OFFSET = 128;

			private int samplingPeriod;

			private long unoptimizedRows;

			StatisticsHeaderEntity(ByteBuffer buffer) {
				 super(buffer);

				 buffer.position(BYTES_OFFSET);

				 this.samplingPeriod = buffer.getInt();

				 this.unoptimizedRows = buffer.getLong();
			}

			StatisticsHeaderEntity(byte[] bytes) {
				 this(ByteBuffer.wrap(bytes));
			}

			StatisticsHeaderEntity(byte samplingPeriod) {
				 super();
				 this.samplingPeriod = samplingPeriod;

				 this.unoptimizedRows = 0;
			}

			@Override
			public byte[] getBytes() {

				 ByteBuffer buffer = ByteBuffer.allocate(BYTES_SIZE);

				 buffer.put(super.getBytes());

				 buffer.position(BYTES_OFFSET);

				 buffer.putInt(this.samplingPeriod);

				 buffer.putLong(this.unoptimizedRows);

				 return buffer.array();
			}

			public int getSamplingPeriod() {
				 return samplingPeriod;
			}

			public void setSamplingPeriod(int samplingPeriod) {
				 this.samplingPeriod = samplingPeriod;
			}

			public long getUnoptimizedRows() {
				 return unoptimizedRows;
			}

			public void setUnoptimizedRows(long unoptimizedRows) {
				 this.unoptimizedRows = unoptimizedRows;
			}
	 }

	 private static class OptimizedRow implements Comparable<OptimizedRow> {

			private Date date;

			private int id;

			private Set<Long> positions;

			OptimizedRow(Date date, int id, long position) {
				 this.date = date;
				 this.id = id;
				 this.positions = new HashSet<Long>();
				 this.positions.add(position);
			}

			public void addPosition(long position) {
				 this.positions.add(position);
			}

			public Date getDate() {
				 return date;
			}

			public int getId() {
				 return id;
			}

			public Set<Long> getPositions() {
				 return positions;
			}

			@Override
			public int hashCode() {
				 int hash = 5;
				 hash = 79 * hash + (this.date != null ? this.date.hashCode() : 0);
				 hash = 79 * hash + this.id;
				 return hash;
			}

			@Override
			public boolean equals(Object obj) {
				 if (obj == null) {
						return false;
				 }
				 if (getClass() != obj.getClass()) {
						return false;
				 }
				 final OptimizedRow other = (OptimizedRow) obj;
				 if (this.date != other.date && (this.date == null || !this.date.equals(other.date))) {
						return false;
				 }
				 if (this.id != other.id) {
						return false;
				 }
				 return true;
			}

			@Override
			public int compareTo(OptimizedRow o) {
				 int result = this.getDate().compareTo(o.getDate());

				 if (result == 0) {
						result = new Integer(this.getId()).compareTo(new Integer(o.getId()));
				 }
				 return result;
			}
	 }
}
